Kinase seed
Substrate seed
Kinase
Substrate
Known (curated)
Novel (Cantley)
Dotted = not flagged functional; Solid = likely functional
Chinmaya Joisa
September 8, 2025
---
title: "Explorer for curated kinase-substrate data"
editor: visual
author: "Chinmaya Joisa"
date: "09/08/2025"
toc: true
toc-depth: 5
toc-title: Table of Contents
highlight-style: pygments
format:
html:
embed-resources: true
code-fold: true
code-tools: true
execute:
echo: false
cache: true
message: false
warning: false
out-width: "100%"
fig-align: center
fig-dpi: 300
---
```{r setup, include=FALSE, cache = FALSE}
require("knitr")
## setting working directory
knitr::opts_knit$set(root.dir = here::here())
set.seed(123)
```
```{r load_libraries, message=FALSE, warning=FALSE}
library(tidyverse)
library(tidygraph)
library(here)
library(igraph)
library(ggraph)
library(scales)
library(readxl)
library(ggpubr)
library(jsonlite)
library(htmltools)
library(ggupset)
library(patchwork)
#read in data
combined_pairs = read_csv(here("results/combined_kinase_substrate_pairs_2025.csv"))
```
```{r, echo=FALSE, results='asis'}
# ---- R: define curated sources used to label evidence -------------------------
curated_sources <- c("PhosphoSitePlus","EPSD","iPTMNet","PhosphoELM","PhosphoNetworks")
# If your combined_pairs already has a logical flag for functionality at the site level,
# collapse it to the edge level. Otherwise, set Likely_functional = FALSE.
# Assumes combined_pairs has columns: Kinase, Substrate, Source, Site, (optional) functional_high
edge_tbl <- combined_pairs %>%
mutate(
evidence = if_else(Source %in% curated_sources, "Known (curated)", "Novel (Cantley)")
) %>%
group_by(Kinase, Substrate) %>%
summarise(
evidence = if_else(any(evidence == "Known (curated)"), "Known (curated)", "Novel (Cantley)"),
Likely_functional = any(Likely_functional),
SiteCount = n_distinct(Site),
Sites = paste(sort(unique(na.omit(Site))), collapse = "; "),
Reference_PMID = paste(sort(unique(Reference_PMID[!is.na(Reference_PMID) & Reference_PMID != ""])), collapse = "; "),
Sources = paste(sort(unique(Source)), collapse = "; "),
.groups = "drop"
) %>%
transmute(
source = Kinase,
target = Substrate,
evidence,
Likely_functional, # logical -> will be serialized to true/false
SiteCount,
Sites,
Reference_PMID,
edgeLabel = paste0(evidence, ifelse(Likely_functional, " · functional", "")),
SourceList = Sources
) %>%
mutate(Likely_functional = if_else(Likely_functional, "Likely functional", "Other/Unknown")) # convert to string for JSON
# Nodes (same as before, but add degree info for tooltips)
node_tbl <- bind_rows(
combined_pairs %>% transmute(Gene = Kinase, Entrez = Kinase_entrez, role = "Kinase"),
combined_pairs %>% transmute(Gene = Substrate, Entrez = Substrate_entrez, role = "Substrate")
) %>%
mutate(Entrez = if_else(is.na(Entrez) | Entrez == "", NA_integer_, Entrez)) %>%
group_by(Gene) %>%
summarise(
Entrez = dplyr::first(na.omit(Entrez)),
role = paste(sort(unique(role)), collapse = ","),
.groups = "drop"
) %>%
arrange(Gene)
# quick degree metadata
deg_tbl <- edge_tbl %>%
count(source, name = "outdeg") %>%
full_join(edge_tbl %>% count(target, name = "indeg"), by = c("source" = "target")) %>%
mutate(outdeg = coalesce(outdeg, 0L),
indeg = coalesce(indeg, 0L)) %>%
transmute(Gene = source, outdeg, indeg) %>%
distinct()
node_tbl <- node_tbl %>%
left_join(deg_tbl, by = "Gene") %>%
mutate(outdeg = coalesce(outdeg, 0L), indeg = coalesce(indeg, 0L))
# JSON payload
nodes_json <- node_tbl %>%
transmute(data = pmap(list(id = Gene, label = Gene, Entrez = Entrez, role = role,
indeg = indeg, outdeg = outdeg),
~ list(id = ..1, label = ..2, Entrez = ..3, role = ..4,
indeg = ..5, outdeg = ..6))) %>%
tidyr::unnest_wider(data)
edges_json <- edge_tbl %>%
transmute(data = pmap(list(source, target, evidence, Likely_functional, SiteCount, Sites, Reference_PMID, edgeLabel, SourceList),
~ list(source = ..1, target = ..2,
evidence = ..3,
Likely_functional = ..4,
SiteCount = ..5,
Sites = ..6,
Reference_PMID = ..7,
edgeLabel = ..6,
SourceList = ..9))) %>%
tidyr::unnest_wider(data)
payload <- list(nodes = nodes_json, edges = edges_json)
json_payload <- jsonlite::toJSON(payload, auto_unbox = TRUE)
json_kinases <- jsonlite::toJSON(sort(node_tbl$Gene[grepl("Kinase", node_tbl$role, fixed = TRUE)]), auto_unbox = TRUE)
json_subs <- jsonlite::toJSON(sort(node_tbl$Gene[grepl("Substrate", node_tbl$role, fixed = TRUE)]), auto_unbox = TRUE)
```
```{r}
# ---- data_tags: embed your JSON payloads in the page as <script type="application/json"> ----
data_tags <- tagList(
tags$script(id = "ks-data", type = "application/json", HTML(json_payload)),
tags$script(id = "kin-data", type = "application/json", HTML(json_kinases)),
tags$script(id = "sub-data", type = "application/json", HTML(json_subs))
)
viewer_tags <- tags$div(
`data-quarto-disable-processing` = "true",
# CSS
tags$style(HTML("
#controls {display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px;align-items:center}
#exportbar {display:flex;gap:10px;flex-wrap:wrap;margin:10px 0 16px 0}
#legend {display:flex;gap:16px;flex-wrap:wrap;align-items:center;margin:8px 0 12px 0;font-size:13px}
#legend .chip{display:inline-flex;align-items:center;gap:6px}
#legend .line{width:32px;height:0;border-top:3px solid #999}
#legend .line.known{border-color:#4c78a8}
#legend .line.novel{border-color:#b279a2}
#legend .dot{width:14px;height:14px;border-radius:50%;display:inline-block;border:1px solid #333}
#legend .kin{background:#e6f0ff}
#legend .sub{background:#fbeadd}
.legend-note{color:#555}
#cy {width:100%;height:720px;border:1px solid #ddd;border-radius:8px}
.label {font-weight:600}
.badge {display:inline-block;padding:2px 8px;border-radius:999px;margin-left:6px;border:1px solid #ccc}
.kbadge{background:#e6f0ff;border-color:#7aa8ff}
.sbadge{background:#fbeadd;border-color:#ffb27a}
.tablewrap{margin-top:14px}
.filters {display:flex;gap:8px;flex-wrap:wrap;margin:6px 0}
.filters input {padding:4px 6px;border:1px solid #ccc;border-radius:6px;font-size:13px}
.tablebox {max-height:360px; overflow:auto; border:1px solid #eee; border-radius:6px}
.tbl {border-collapse:collapse;width:100%}
.tbl th, .tbl td{border-bottom:1px solid #eee;padding:6px 8px;font-size:13px;text-align:left;white-space:nowrap}
.tbl th{background:#fafafa; position: sticky; top: 0; z-index: 1;}
.btn{padding:6px 10px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer}
.btn:hover{background:#f6f6f6}
")),
# Controls
tags$div(
id = "controls",
tags$label(class="label", "Kinase:", `for`="kinSel"),
tags$input(id="kinSel", list="kinList", placeholder="Type a kinase…", style="min-width:240px"),
tags$datalist(id="kinList"),
tags$span(class="badge kbadge", "Kinase seed"),
tags$label(class="label", "Substrate:", `for`="subSel", style="margin-left:16px"),
tags$input(id="subSel", list="subList", placeholder="Type a substrate…", style="min-width:240px"),
tags$datalist(id="subList"),
tags$span(class="badge sbadge", "Substrate seed"),
tags$label(class="label", "Layout:", `for`="layoutSel", style="margin-left:16px"),
tags$select(
id="layoutSel",
tags$option(value="cose","COSE"),
tags$option(value="fcose","fCoSE"),
tags$option(value="concentric","Concentric"),
tags$option(value="breadthfirst","Breadthfirst")
),
tags$label(tags$input(type="checkbox", id="labelsChk", checked=NA), " Node labels"),
tags$label(tags$input(type="checkbox", id="edgeLabelsChk", checked=NA), " Edge labels"),
tags$label(tags$input(type="checkbox", id="arrowsChk", checked=NA), " Arrows"),
tags$label(tags$input(type="checkbox", id="onlyFunctionalChk"), " Only functional")
),
# Legend
tags$div(
id="legend",
tags$span(class="chip", tags$span(class="dot kin"), "Kinase"),
tags$span(class="chip", tags$span(class="dot sub"), "Substrate"),
tags$span(class="chip", tags$span(class="line known"), "Known (curated)"),
tags$span(class="chip", tags$span(class="line novel"), "Novel (Cantley)"),
tags$span(class="legend-note", "Dotted = not flagged functional; Solid = likely functional")
),
# Export bar
tags$div(
id="exportbar",
tags$button(id="btnPng", class="btn", "Export PNG"),
tags$button(id="btnPdf", class="btn", "Export PDF"),
tags$button(id="btnCsvEdges", class="btn", "Download Edges CSV")
),
# Graph
tags$div(id="cy"),
# Edges table with filters
tags$div(
class="tablewrap",
tags$h4("Edges in View"),
tags$div(id="edgesFilters", class="filters"),
tags$div(id="edgesTable", class="tablebox")
),
# JS libs
tags$script(src="https://unpkg.com/cytoscape@3.26.0/dist/cytoscape.min.js"),
tags$script(src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"),
# Viewer JS
tags$script(HTML("
(function(){
const data = JSON.parse(document.getElementById('ks-data').textContent);
const kinases = JSON.parse(document.getElementById('kin-data').textContent);
const substrates = JSON.parse(document.getElementById('sub-data').textContent);
// populate datalists
const kinDL = document.getElementById('kinList');
kinases.forEach(g => { const opt = document.createElement('option'); opt.value = g; kinDL.appendChild(opt); });
const subDL = document.getElementById('subList');
substrates.forEach(g => { const opt = document.createElement('option'); opt.value = g; subDL.appendChild(opt); });
const cy = cytoscape({
container: document.getElementById('cy'),
elements: [],
style: [
// Nodes
{ selector: 'node',
style: {
'shape': 'round-rectangle',
'background-color': '#4c78a8', // default; role-specific below
'label': 'data(label)',
'font-size': 11,
'color': '#222',
'text-opacity': 1,
'text-margin-y': -2,
'text-halign': 'center',
'text-valign': 'center',
'border-color': '#1f2a44',
'border-width': 0.8,
'width': 'label',
'height': 'label',
'padding': '6px'
}
},
{ selector: 'node[role *= \"Substrate\"]:not([role *= \"Kinase\"])',
style: { 'background-color': '#fbeadd', 'border-color': '#b27f52' } },
{ selector: 'node[role *= \"Kinase\"]',
style: { 'background-color': '#e6f0ff', 'border-color': '#7aa8ff' } },
// highlight seeds (optional classes applied later)
{ selector: 'node.qk', style: { 'border-width': 2.5 } },
{ selector: 'node.qs', style: { 'border-width': 2.5 } },
// Edges
{ selector: 'edge',
style: {
'width': 1.6,
'curve-style': 'bezier',
'line-color': '#999',
'target-arrow-color': '#999',
'target-arrow-shape': 'triangle',
'label': 'data(edgeLabel)', // edge labels!
'font-size': 9,
'text-rotation': 'autorotate',
'text-background-color': '#ffffff',
'text-background-opacity': 0.85,
'text-background-padding': 2,
'text-margin-y': -2
}
},
// Evidence → color
{ selector: 'edge[evidence = \"Known (curated)\"]',
style: { 'line-color': '#4c78a8', 'target-arrow-color': '#4c78a8' } },
{ selector: 'edge[evidence = \"Novel (Cantley)\"]',
style: { 'line-color': '#b279a2', 'target-arrow-color': '#b279a2' } },
// Functional flag → line style
{ selector: 'edge[Likely_functional = \"Likely functional\"]',
style: { 'line-style': 'solid' } },
{ selector: 'edge[Likely_functional = \"Other/Unknown\"]',
style: { 'line-style': 'dotted' } }
],
layout: { name: 'cose' }
});
function setNodeLabels(show){
cy.style().selector('node').style('label', show ? 'data(label)' : '').update();
}
function setEdgeLabels(show){
cy.style().selector('edge').style('label', show ? 'data(edgeLabel)' : '').update();
}
function setArrows(show){
cy.style().selector('edge').style('target-arrow-shape', show ? 'triangle' : 'none').update();
}
function isFunctionalEdge(e){
const v = (e.data('Likely_functional') ?? '').trim();
return v === 'Likely functional';
}
function applyFunctionalFilter(onlyFunctional){
if(!onlyFunctional){
cy.edges().style('display', 'element');
cy.nodes().style('display', 'element');
} else {
// show only edges whose attribute equals Likely functional
cy.edges().forEach(e => {
e.style('display', isFunctionalEdge(e) ? 'element' : 'none');
});
// hide nodes that end up with no visible incident edges
cy.nodes().forEach(n => {
const hasVisibleEdge = n.connectedEdges(':visible').length > 0;
n.style('display', hasVisibleEdge ? 'element' : 'none');
});
}
// (optional) repack after filtering
cy.layout({ name: 'cose', animate: false }).run();
}
function refreshEdgesTable(){
const rows = cy.edges(':visible').map(e => [
e.data('source'),
e.data('target'),
e.data('evidence'),
e.data('Likely_functional'),
e.data('SiteCount'),
e.data('Sites'),
e.data('Reference_PMID'),
e.data('SourceList') ?? ''
]);
renderFilterableTable('edgesFilters','edgesTable',
['From','To','Evidence','Functional','#Sites','Sites','Reference_PMID','Sources'], rows);
}
function renderFilterableTable(filtersId, tableBoxId, headers, rows){
const fwrap = document.getElementById(filtersId);
const box = document.getElementById(tableBoxId);
fwrap.innerHTML = '';
box.innerHTML = '';
const filters = headers.map(h => {
const inp = document.createElement('input');
inp.type = 'text';
inp.placeholder = 'Filter ' + h;
fwrap.appendChild(inp);
return inp;
});
const table = document.createElement('table');
table.className = 'tbl';
const thead = document.createElement('thead');
const trh = document.createElement('tr');
headers.forEach(h => { const th = document.createElement('th'); th.textContent = h; trh.appendChild(th); });
thead.appendChild(trh); table.appendChild(thead);
const tbody = document.createElement('tbody'); table.appendChild(tbody); box.appendChild(table);
function passes(row, query){
for(let i=0;i<query.length;i++){
const q = query[i];
if(q && !String(row[i] ?? '').toLowerCase().includes(q)) return false;
}
return true;
}
function draw(){
const q = filters.map(x => x.value.trim().toLowerCase());
tbody.innerHTML = '';
rows.forEach(r => {
if(!passes(r, q)) return;
const tr = document.createElement('tr');
r.forEach(v => { const td = document.createElement('td'); td.textContent = (v ?? '').toString(); tr.appendChild(td); });
tbody.appendChild(tr);
});
}
filters.forEach(inp => inp.addEventListener('input', draw));
draw();
}
function incidentSubgraph(seeds){
const nodeIndex = new Map(data.nodes.map(n => [n.id, n]));
const validSeeds = seeds.filter(s => nodeIndex.has(s));
if(!validSeeds.length){ return {nodes:[], edges:[]}; }
const edges = data.edges.filter(e => validSeeds.includes(e.source) || validSeeds.includes(e.target));
if(!edges.length){ return {nodes:[], edges:[]}; }
const nset = new Set(); edges.forEach(e => { nset.add(e.source); nset.add(e.target); });
const nodes = Array.from(nset).map(id => ({
data: nodeIndex.get(id),
classes: (validSeeds.includes(id) ? (seeds[0]===id ? 'qk' : (seeds[1]===id ? 'qs' : '')) : '')
}));
return {nodes: nodes.map(n => ({ data: n.data, classes: n.classes })), edges: edges.map(e => ({ data: e }))};
}
function relayout(name){
const opts = (name==='concentric') ? { name, minNodeSpacing: 20 } :
(name==='breadthfirst') ? { name, directed: true, padding: 10 } :
(name==='fcose') ? { name, quality: 'proof', randomize: true } :
{ name: 'cose', animate: false };
cy.layout(opts).run();
}
function render(){
const kin = document.getElementById('kinSel').value.trim();
const sub = document.getElementById('subSel').value.trim();
const seeds = [kin, sub].filter(s => s && s.length);
if(!seeds.length) return;
const elems = incidentSubgraph(seeds);
cy.elements().remove();
cy.add(elems.nodes);
cy.add(elems.edges);
relayout(document.getElementById('layoutSel').value);
// update table
const rows = cy.edges().map(e => [
e.data('source'),
e.data('target'),
e.data('evidence'),
e.data('Likely_functional'),
e.data('SiteCount'),
e.data('Sites'),
e.data('Reference_PMID'),
e.data('SourceList') ?? ''
]);
renderFilterableTable('edgesFilters','edgesTable',
['From','To','Evidence','Functional','#Sites','Sites','Reference_PMID','Sources'], rows);
const onlyFn = document.getElementById('onlyFunctionalChk').checked;
applyFunctionalFilter(onlyFn);
refreshEdgesTable();
}
// UI wiring
document.getElementById('kinSel').addEventListener('change', render);
document.getElementById('subSel').addEventListener('change', render);
document.getElementById('layoutSel').addEventListener('change', () => relayout(document.getElementById('layoutSel').value));
document.getElementById('labelsChk').addEventListener('change', e => setNodeLabels(e.target.checked));
document.getElementById('edgeLabelsChk').addEventListener('change', e => setEdgeLabels(e.target.checked));
document.getElementById('arrowsChk').addEventListener('change', e => setArrows(e.target.checked));
document.getElementById('onlyFunctionalChk').addEventListener('change', () => {
applyFunctionalFilter(document.getElementById('onlyFunctionalChk').checked);
refreshEdgesTable();
});
// Exports
function download(filename, blob){
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
}
function csvBlob(rows, header){
const esc = v => '\"' + String(v ?? '').replaceAll('\"','\"\"') + '\"';
const lines = [header.map(esc).join(',')].concat(rows.map(r => r.map(esc).join(',')));
return new Blob([lines.join('\\n')], {type:'text/csv'});
}
document.getElementById('btnPng').addEventListener('click', () => {
const uri = cy.png({full:true, scale:2});
fetch(uri).then(r => r.blob()).then(b => download('kinome_network.png', b));
});
document.getElementById('btnPdf').addEventListener('click', () => {
const uri = cy.png({full:true, scale:2});
fetch(uri).then(r => r.blob()).then(b => {
const reader = new FileReader();
reader.onload = function(){
const { jsPDF } = window.jspdf;
const pdf = new jsPDF({orientation:'landscape', unit:'pt', format:'a4'});
const img = reader.result;
const pageW = pdf.internal.pageSize.getWidth();
const pageH = pdf.internal.pageSize.getHeight();
pdf.addImage(img, 'PNG', 20, 20, pageW-40, pageH-40);
pdf.save('kinome_network.pdf');
};
reader.readAsDataURL(b);
});
});
document.getElementById('btnCsvEdges').addEventListener('click', () => {
const rows = cy.edges().map(e => [
e.data('source'),
e.data('target'),
e.data('evidence'),
e.data('Likely_functional'),
e.data('SiteCount'),
e.data('Sites'),
e.data('Reference_PMID'),
e.data('SourceList') ?? ''
]);
const blob = csvBlob(rows, ['From','To','Evidence','Functional','#Sites','Sites','Reference_PMID','Sources']);
download('kinome_edges.csv', blob);
});
document.getElementById('btnCsvEdges').addEventListener('click', () => {
const rows = cy.edges(':visible').map(e => [
e.data('source'),
e.data('target'),
e.data('evidence'),
e.data('Likely_functional'),
e.data('SiteCount'),
e.data('Sites'),
e.data('Reference_PMID'),
e.data('SourceList') ?? ''
]);
const blob = csvBlob(rows, ['From','To','Evidence','Functional','#Sites','Sites','Reference_PMID','Sources']);
download('kinome_edges.csv', blob);
});
// Defaults
setNodeLabels(true);
setEdgeLabels(true);
setArrows(true);
})();
"))
)
browsable(tagList(data_tags, viewer_tags))
```